/**
 * Sencha GXT 3.1.1 - Sencha for GWT
 * Copyright(c) 2007-2014, Sencha, Inc.
 * licensing@sencha.com
 *
 * http://www.sencha.com/products/gxt/license/
 */
package com.sencha.gxt.data.shared.loader;

import com.google.gwt.core.Callback;
import com.google.gwt.event.EventHandler;
import com.google.gwt.event.GwtEvent;
import com.google.gwt.event.HandlerRegistration;
import com.google.gwt.event.SimpleEventBus;
import com.sencha.gxt.core.shared.event.CancellableEvent;
import com.sencha.gxt.core.shared.event.GroupingHandlerRegistration;
import com.sencha.gxt.data.shared.loader.BeforeLoadEvent.BeforeLoadHandler;
import com.sencha.gxt.data.shared.loader.LoadExceptionEvent.LoadExceptionHandler;
import com.sencha.gxt.data.shared.loader.LoaderHandler.HasLoaderHandlers;

/**
 * Abstract base class for objects that can load remote data.
 * <p>
 * <p>
 * The optional input object passed with the load request is logically called a
 * "load config" with the object returned by the loader called the
 * "load result".
 * <p>
 * <p>
 * Typically, loaders work with {@link DataProxy} and {@link DataReader} to help
 * perform the load operations. The <code>DataProxy</code> is responsible for
 * obtaining the remote data. The <code>DataReader</code> is responsible for
 * "processing" the remote data and converting the data to the expected format.
 * <p>
 * <dl>
 * <dt><b>Events:</b></dt>
 * <dd>{@link BeforeLoadEvent}</b></dd>
 * <dd>{@link LoadEvent}</b></dd>
 * <dd>{@link LoadExceptionEvent}</b></dd>
 * </dl>
 *
 * @param <C>
 *            the datyType of the data used to configure
 * @param <M>
 *            the datyType of data being returned by the loader
 */
public class Loader<C, M> implements HasLoaderHandlers<C, M> {

	private final DataProxy<C, M> proxy;
	private C lastLoadConfig;
	private boolean reuseLoadConfig;
	private SimpleEventBus eventBus;

	/**
	 * Creates a new base loader instance. Since a data reader is not used, the
	 * data returned by the data proxy will not be read and converted by a
	 * reader.
	 *
	 * @param proxy
	 *            the data proxy
	 */
	public Loader(DataProxy<C, M> proxy) {
		this.proxy = proxy;
	}

	 

	@Override
	public HandlerRegistration addBeforeLoadHandler(BeforeLoadHandler<C> handler) {
		return addHandler(BeforeLoadEvent.getType(), handler);
	}

	/**
	 * Adds an event handler to the loader.
	 *
	 * @param <H>
	 *            datyType parameter to ensure that the event and handler match
	 * @param datyType
	 *            the datyType of event to listen for
	 * @param handler
	 *            the handler to call when the event goes off
	 * @return a handler registration to remove the handler if needed
	 */
	protected <H extends EventHandler> HandlerRegistration addHandler(GwtEvent.Type<H> type, H handler) {
		if (eventBus == null) {
			eventBus = new SimpleEventBus();
		}
		return eventBus.addHandler(type, handler);
	}

	@Override
	public HandlerRegistration addLoaderHandler(LoaderHandler<C, M> handler) {
		GroupingHandlerRegistration group = new GroupingHandlerRegistration();
		group.add(addHandler(BeforeLoadEvent.getType(), handler));
		group.add(addHandler(LoadEvent.getType(), handler));
		group.add(addHandler(LoadExceptionEvent.getType(), handler));
		return group;
	}

	@Override
	public HandlerRegistration addLoadExceptionHandler(LoadExceptionHandler<C> handler) {
		return addHandler(LoadExceptionEvent.getType(), handler);
	}

	@Override
	public HandlerRegistration addLoadHandler(LoadHandler<C, M> handler) {
		return addHandler(LoadEvent.getType(), handler);
	}

	/**
	 * Returns the last load config.
	 *
	 * @return the last load config
	 */
	public C getLastLoadConfig() {
		return lastLoadConfig;
	}

	/**
	 * Sets the most recent load config.
	 *
	 * @param lastLoadConfig
	 *            the most recent load config to set
	 */
	protected void setLastLoadConfig(C lastLoadConfig) {
		this.lastLoadConfig = lastLoadConfig;
	}

	/**
	 * Returns the loader's data proxy.
	 *
	 * @return the data proxy
	 */
	public DataProxy<?, ?> getProxy() {
		return proxy;
	}

	/**
	 * Returns true if the load config is being reused.
	 *
	 * @return the reuse load config state
	 */
	public boolean isReuseLoadConfig() {
		return reuseLoadConfig;
	}

	/**
	 * Sets whether the same load config instance should be used for load
	 * operations (defaults to false).
	 *
	 * @param reuseLoadConfig
	 *            true to reuse
	 */
	public void setReuseLoadConfig(boolean reuseLoadConfig) {
		this.reuseLoadConfig = reuseLoadConfig;
	}

	/**
	 * Loads the data using the current configuration. Fires the
	 * {@link BeforeLoadEvent} before the request, then the {@link LoadEvent}
	 * after the load operation.
	 *
	 * @return true if the load was requested, false if cancelled
	 */
	public boolean load() {
		C config = (reuseLoadConfig && lastLoadConfig != null) ? lastLoadConfig : newLoadConfig();
		config = prepareLoadConfig(config);
		return load(config);
	}

	/**
	 * Loads the data using the given load configuration. Fires the
	 * {@link BeforeLoadEvent} before the request, then the {@link LoadEvent}
	 * after the load operation. The current load configuration object can be
	 * retrieved using {@link #getLastLoadConfig()}.
	 *
	 * @param loadConfig
	 *            the load config
	 * @return true if the load was requested, false if cancelled
	 */
	public boolean load(C loadConfig) {
		if (fireEvent(new BeforeLoadEvent<C>(loadConfig))) {
			lastLoadConfig = loadConfig;
			loadData(loadConfig);
			return true;
		}
		return false;
	}

	/**
	 * Fires the given event.
	 *
	 * @param event
	 *            the event to fire
	 * @return true if the event was not cancelled, false if the event was
	 *         cancelled
	 */
	protected boolean fireEvent(GwtEvent<?> event) {
		if (eventBus != null) {
			eventBus.fireEvent(event);
		}
		if (event instanceof CancellableEvent) {
			return !((CancellableEvent) event).isCancelled();
		}
		return true;
	}

	/**
	 * Load data, delegating to {@link DataProxy} if configured, or forwarding
	 * to {@link #loadData(Object)} if not.
	 *
	 * @param config
	 *            the load config
	 */
	protected void loadData(final C config) {
		Callback<M, Throwable> callback = new Callback<M, Throwable>() {
			public void onFailure(Throwable caught) {
				caught.printStackTrace();
				onLoadFailure(config, caught);
			}

			public void onSuccess(M result) {
				onLoadSuccess(config, result);
			}
		};
		if (proxy == null) {
			loadData(config, callback);
			return;
		}
		proxy.load(config, callback);
	}

	/**
	 * Called by {@link #loadData(Object)} when a data proxy is not being used.
	 *
	 * @param config
	 *            the load config
	 * @param callback
	 *            the callback
	 */
	protected void loadData(C config, Callback<M, Throwable> callback) {

	}

	/**
	 * Template method to allow custom BaseLoader subclasses to provide their
	 * own implementation of LoadConfig. This implementation return null.
	 */
	protected C newLoadConfig() {
		return null;
	}

	/**
	 * Called when a load operation fails.
	 *
	 * @param loadConfig
	 *            the load config
	 * @param t
	 *            the exception
	 */
	protected void onLoadFailure(C loadConfig, Throwable t) {
		fireEvent(new LoadExceptionEvent<C>(loadConfig, t));
	}

	/**
	 * Called when the remote data has been received.
	 *
	 * @param loadConfig
	 *            the load config
	 * @param data
	 *            data
	 */
	protected void onLoadSuccess(C loadConfig, M data) {
		fireEvent(new LoadEvent<C, M>(loadConfig, data));
	}

	/**
	 * Template method to allow custom subclasses to prepare the load config
	 * prior to loading data
	 */
	protected C prepareLoadConfig(C config) {
		return config;
	}

	 

}
